Creating a Virtual Piano

Creating a Virtual Piano

Playing with additive synthesis in Python.

First, some necessary imports:

import matplotlib.pyplot as plt
import numpy as np
import IPython as ju
from IPython.display import Audio
plt.rcParams["figure.figsize"] = (20,5)

The following equation will give the frequency \(f\) of the \(n^{th}\) key: \[f(n) = 440 * 2^\frac{n-49}{12}\]

This equation is from here .

We use this as a utility function named key2freq in the VirtualPiano class bellow.

class VirtualPiano:
    def __init__(self,
                 t_duration,
                 sample_freq=48000):
        self.t_duration = t_duration
        self.sample_freq = sample_freq
        self.sequence = []

        # elements to pass through sine function:
        self.t = np.linspace(0, t_duration,
                           num=int(self.sample_freq*self.t_duration))
        
    def sound(self, freq: float):
        t = self.t
        Y = np.sin(2 * np.pi * freq * t) * np.exp(-0.0004 * 2 * np.pi * freq * t)
        Y += np.sin(2 * 2 * np.pi * freq * t) * np.exp(-0.0004 * 2 * np.pi * freq * t) / 2
        Y += np.sin(3 * 2 * np.pi * freq * t) * np.exp(-0.0004 * 2 * np.pi * freq * t) / 4
        Y += np.sin(4 * 2 * np.pi * freq * t) * np.exp(-0.0004 * 2 * np.pi * freq * t) / 8
        Y += np.sin(5 * 2 * np.pi * freq * t) * np.exp(-0.0004 * 2 * np.pi * freq * t) / 16
        Y += np.sin(6 * 2 * np.pi * freq * t) * np.exp(-0.0004 * 2 * np.pi * freq * t) / 32
        Y += Y * Y * Y
        Y *= 1 + 16 * t * np.exp(-6 * t)
        
        return Y

    def key2freq(self, n: int):
        """Input: 0-88 (piano key) (0 for pause)
           Output: frequency of key in hertz"""

        if 0 < n and n <= 88:
            return 440 * 2**((n-49)/12)
        elif n == 0:
            return 0
        else:
            raise ValueError("Only keys 0-88 allowed")
    
    def waveform(self):
        """Generate continuous waveform
           of the sequence of notes/frequencies."""

        waveform = []
        for key in self.sequence:
            freq = self.key2freq(key)
            oscillator = self.sound(freq)
            for val in oscillator:
                waveform.append(val)
        return np.asarray(waveform)

Example: Scale of C-Major

# Initialize piano with duration of 0.5s for each note:
piano = VirtualPiano(t_duration=0.5)

# Add sequence of keys to play:
piano.sequence = [40, 42, 44, 45, 47, 49, 51]

# Play waveform of notes with sampling frequency of 48 kHz
c_major_scale_waveform = piano.waveform()
Audio(c_major_scale_waveform, rate=48000)

Example: Prelude in C Major - Bach

# Initialize new piano
piano = VirtualPiano(t_duration=0.175)

# Sequence of keys:
piano.sequence = [40, 44, 47, 52, 56, 47, 52, 56, 
                  40, 44, 47, 52, 56, 47, 52, 56, 
                  40, 42, 49, 54, 57, 49, 54, 57,
                  40, 42, 49, 54, 57, 49, 54, 57,
                  39, 42, 47, 54, 57, 47, 54, 57,
                  40, 44, 47, 52, 56, 47, 52, 56,
                  40, 44, 47, 52, 56, 47, 52, 56,
                  40, 44, 49, 56, 61, 49, 56, 61,
                  40, 44, 49, 56, 61, 49, 56, 61,
                  40, 42, 46, 49, 54, 46, 49, 54,
                  40, 42, 46, 49, 54, 46, 49, 54,
                  39, 42, 47, 54, 59, 47, 54, 59,
                  39, 42, 47, 54, 59, 47, 54, 59,
                  39, 40, 44, 47, 52, 44, 47, 52,
                  39, 40, 44, 47, 52, 44, 47, 52,
                  37, 40, 44, 47, 52, 44, 47, 52,
                  37, 40, 44, 47, 52, 44, 47, 52,]

prelude_waveform = piano.waveform()

plt.plot(prelude_waveform[:20000])
Audio(prelude_waveform, rate=48000)

png

framerate = 44100 # 44.1 kHz sampling frequency
t = np.linspace(0, 5, num=framerate*5)

sound = np.sin(np.pi*220*t) + np.sin(np.pi*330*t)

print("Framerate=44100")
plt.plot(np.asarray([*range(1000)]), sound[:1000], "go")
plt.ylabel("sampling_freq:\n44.1kHz")
plt.show()
Framerate=44100

png

Audio(data, rate=framerate)
t_dur = 0.5
samp = 44100
x = np.linspace(0.0, t_dur, num=int(t_dur*samp))
np.mod(x ,1)
array([0.00000000e+00, 2.26767654e-05, 4.53535308e-05, ...,
       4.99954646e-01, 4.99977323e-01, 5.00000000e-01])
t = np.linspace(0,5, num=44100*5)

freq = 32.7032 # this is the frequency of the 4th key, C,contra_octave

data = 0.3 * np.sin(1*np.pi*np.sin(np.pi*t**2)*32.7032)
data += np.sin(np.pi*np.sin(np.pi*t**2)*65.4064) / 4
data += np.sin(np.pi*np.sin(np.pi*t**2)*130.813) / 8

reverb = lambda arr: np.concatenate((np.zeros(88200), arr))
data += reverb(data)[:220500]
plt.plot(np.concatenate((data[:50000]))
Audio((data), rate=44100)

png

t = np.linspace(0,0.25, num=int(48000*0.25))
freq = 220
s = np.sin(2 * np.pi * freq * t) * np.exp(-0.0004 * 2 * np.pi * freq * t) + np.sqrt(np.pi * 220 * t)/1200
s = np.concatenate((s,s,s,s,s,s,s,s))
plt.plot(s)
Audio(s, rate=48000)

png


fade_in = np.sin(np.pi * 220 * t) * np.sqrt(np.pi * 220 * t)
fade_ins = np.concatenate((fade_in, fade_in, fade_in, fade_in))

fade_out = np.sin(np.pi * 220 * t) * np.exp(-0.000004 * t * 220)
fade_outs = np.concatenate((fade_out, fade_out, fade_out, fade_out))

print("Fade-in:")
ju.display.display(Audio(fade_ins, rate=48000))
plt.plot(fade_ins)
plt.show()

print("Fade-out:")
ju.display.display(Audio(fade_outs, rate=48000))
plt.plot(fade_outs)
plt.show()

Fade-in:

png

Fade-out:

png

Non-trigonometric Oscillators

Sawtooth Oscillators

\(f(t) = mod(t, 1)\)

This is a WIP.

t = np.linspace(0, 5, num=48000*5)
sawtooth = lambda t_i: -(2/np.pi) * np.asarray([*sum((((-1)**k)/k)*np.sin(2*np.pi*k*t_i*440) for k in range(1, 20000))])

# wav = (sawtooth(t))

# ju.display.display(Audio(wav, rate=48000))
# plt.plot(wav[:20000])
# plt.show()